import tarfile
import zipfile
import re
import string
import os.path
from typing import List

from .helpers import InMemoryOrDiskFile


class Archive(object):
    """Archive provides a consistent interface for unpacking
    compressed file.
    """

    def __init__(self, filename, fileobj):
        self._filename = filename
        self._fileobj = fileobj
        self._file = None
        self._names = None
        self._read = None

    @property
    def filename(self):
        return self._filename

    @property
    def names(self):
        """If name list is not required, do not get it."""
        if self._file is None:
            self._prepare()
        if not hasattr(self, '_namelist'):
            self._namelist = self._names()
        return self._namelist

    def close(self):
        """Close file object."""
        if self._file is not None:
            self._file.close()
        if hasattr(self, '_namelist'):
            del self._namelist
        self._filename = self._fileobj = None
        self._file = self._names = self._read = None

    def read(self, filename):
        """Read one file from archive."""
        if self._file is None:
            self._prepare()
        return self._read(filename)

    def unpack(self, to_path):
        """Unpack compressed files to path."""
        if self._file is None:
            self._prepare()
        self._safe_extractall(to_path)

    def _prepare(self):
        if self._filename.endswith(('.tar.gz', '.tar.bz2', '.tar.xz')):
            self._prepare_tarball()
        # An .egg file is actually just a .zip file
        # with a different extension, .whl too.
        elif self._filename.endswith(('.zip', '.egg', '.whl')):
            self._prepare_zip()
        else:
            raise ValueError("unreadable: {0}".format(self._filename))

    def _safe_extractall(self, to_path='.'):
        unsafe = []
        for name in self.names:
            if not self.is_safe(name):
                unsafe.append(name)
        if unsafe:
            raise ValueError("unsafe to unpack: {}".format(unsafe))
        self._file.extractall(to_path)

    def _prepare_zip(self):
        self._file = zipfile.ZipFile(self._fileobj)
        self._names = self._file.namelist
        self._read = self._file.read

    def _prepare_tarball(self):
        # tarfile has no read method
        def _read(filename):
            f = self._file.extractfile(filename)
            return f.read()

        self._file = tarfile.open(mode='r:*', fileobj=self._fileobj)
        self._names = self._file.getnames
        self._read = _read

    def is_safe(self, filename):
        return not (
            filename.startswith(("/", "\\")) or (
                len(filename) > 1 and filename[1] == ":"
                and filename[0] in string.ascii_letters
            ) or re.search(r"[.][.][/\\]", filename)
        )

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


def parse_top_levels(file: InMemoryOrDiskFile) -> List[str]:
    """Read top level names from compressed file."""
    assert (not file.opened())

    with file as stream:
        with Archive(file.name, stream) as archive:
            top_level_file = None
            top_level_dirs = []
            for name in archive.names:
                basename = os.path.basename(name)
                if basename == 'top_level.txt':
                    top_level_file = name
                    break
                if basename == '__init__.py':
                    dir = os.path.dirname(name)
                    if '' == os.path.dirname(dir):  # Root.
                        top_level_dirs.append(dir)
            if top_level_file is None:
                if top_level_dirs:
                    return top_level_dirs
                return []

            top_level_txt = archive.read(top_level_file).decode('utf-8')
            return [
                name.replace('/', '.') for name in top_level_txt.splitlines()
                if name
            ]
